<?php
declare (strict_types=1);

namespace app\exam\logic\admin;

use app\common\event\subscribes\attach\Attach as AttachSubscribes;
use app\common\exception\LogicException;
use app\common\library\Excel;
use app\common\logic\Attach;
use Exception;
use PhpOffice\PhpWord\Element\{Image, Text, TextBreak};
use phpDocumentor\Reflection\Types\Self_;
use PhpOffice\PhpWord\IOFactory;
use think\annotation\Inject;
use think\db\exception\DbException;
use think\facade\Event;
use think\File;
use think\Paginator;

/**
 * Class QuestionBankTopic
 *
 * @package app\exam\logic\admin
 */
class QuestionBankTopic
{
    /**
     * @Inject()
     *
     * @var QuestionTopic
     */
    protected $questionTopicLogic;

    /**
     * @Inject()
     *
     * @var QuestionPoint
     */
    protected $questionPointLogic;

    /**
     * @Inject()
     *
     * @var Attach
     */
    protected $attachLogic;

    /**
     * @Inject()
     *
     * @var QuestionDifficult
     */
    protected $questionDifficultLogic;

    /**
     * getTopicListByBankId
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/3/23
     *
     * @param int $bankId
     * @param array $where
     * @return Paginator
     * @throws DbException
     */
    public function getTopicListByBankId(int $bankId, array $where = []): Paginator
    {
        $where[] = ['question_bank_id', '=', $bankId];

        return $this->questionTopicLogic->getList($where);
    }

    /**
     * 导入试题
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/3/29
     *
     * @param int $bankId
     * @param int $attachId
     * @return int
     */
    public function doImport(int $bankId, int $attachId): int
    {
        $filepath = $this->attachLogic->where('id', $attachId)->findOrEmpty()->getAttr('file_path');
        if (is_file($filepath)) {
            $this->questionTopicLogic->startTrans();
            try {
                // 读取试题数据
                $datas = $this->readDatasFromExcelFile($filepath);
                $topics = $this->parseTopicDatas($datas, $bankId);
                // 保存试题
                $successCount = $this->saveTopics($topics);
                $this->questionTopicLogic->commit();

                // 删除附件
                Event::subscribe(AttachSubscribes::class);
                Event::trigger('AttachTrueDeleted', $attachId);

                return $successCount;
            } catch (Exception $e) {
                $this->questionTopicLogic->rollback();
                throw new LogicException('导入失败');
            }
        } else {
            throw new LogicException('请重新上传试题文件');
        }
    }

    /**
     * 从word文件中读取试题数据
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/3/29
     *
     * @desc 暂时只支持: 单选/多选/判断/不定项/简答/材料
     * @TODO 填空/完型填空
     * @param      $filepath
     * @param bool $handleImage
     * @return array
     * @throws Exception
     */
    private function readDatasFromWordFile($filepath, bool $handleImage = true): array
    {
        $phpword = IOFactory::load($filepath);
        $sections = $phpword->getSections();
        $datas = [];
        // 存放大题
        $topic = [];
        // 存放小题
        $subTopic = [];
        $isSub = false;
        foreach ($sections as $section) {
            foreach ($section->getElements() as $element) {
                $text = $this->parseElement($element, $handleImage);

                // 解析分割的数据
                if (preg_match('/^\#{5}/', $text)) {// 大题,每道大题有5个#号分割
                    // 新的一题
                    $datas[] = $topic;
                    // 重置旧数据
                    $topic = [];
                    $isSub = false;
                    // 去除#号
                    $text = preg_replace('/^\#{5}/', '', $text, 1);

                } else if (preg_match('/^\#{4}/', $text)) { // 小题,每道小题前有4个#号分割
                    if ($subTopic) {
                        !isset($topic['sub_topic']) && $topic['sub_topic'] = [];
                        $topic['sub_topic'][] = $subTopic;
                    }
                    // 重置旧数据
                    $subTopic = [];
                    $isSub = true;
                    // 去除#号
                    $text = preg_replace('/^\#{4}/', '', $text, 1);
                }

                // 去除空行
                if ($text) {
                    // 解析当前行数据
                    $result = $this->parseLineText($text);
                    if ($isSub) {// 小题统一存放
                        $subTopic = $this->appendItemData($subTopic, $result);
                    } else {
                        $topic = $this->appendItemData($topic, $result);
                    }
                }
            }
            // 最后一道小题
            if ($subTopic) {
                !isset($topic['sub_topic']) && $topic['sub_topic'] = [];
                $topic['sub_topic'][] = $subTopic;
            }
            // 最后一道大题
            if ($topic) $datas[] = $topic;

            $topic = $subTopic = [];

        }

        return $datas;
    }

    /**
     * 从word文件中读取试题数据
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/3/29
     *
     * @desc 暂时只支持: 单选/多选/判断/不定项/简答/材料
     * @TODO 填空/完型填空
     * @param      $filepath
     * @param bool $handleImage
     * @return array
     * @throws Exception
     */
    private function readDatasFromExcelFile($filepath, bool $handleImage = true): array
    {
        $data = self::import($filepath);
//        return $data;
        $datas = [];
        // 遍历原始数据数组
        foreach ($data as $index => $item) {
            // 生成新的数据格式
            if ($index === 0) {
                continue;
            } else {
                // 否则，重构每一行数据
                $mainTopic = '';
                $subTopic = '';
                $newRow = [
                    'question_type' => $item[1],
                    'options'       => [
                    ],
                    'difficulty'    => $item[11] ?? '',
                    'analyze'       => $item[12] ?? '',
                ];
                if ($item[1] == '简答题') {
                    $newRow['options'][] = [
                        'key'   => '参考答案',
                        'value' => $item[9] ?? ''
                    ];
                }else{
                    $newRow['answer'] = $item[9] ?? '';
                }
                if (!empty($item[10])) {
                    $newRow['point'] = $item[10] ?? '';
                }
                //处理选项
                $keys = ['A', 'B', 'C', 'D', 'E'];

                for ($i = 4; $i <= 8; $i++) {
                    if (!empty($item[$i])) {
                        $newRow['options'][] = [
                            'key'   => $keys[$i - 4], // 从0开始索引
                            'value' => $item[$i]
                        ];
                    }
                }
                // 检查字符串是否包含'-'
                if (strpos((string)$item[0], '-') === false) {
                    $mainTopic = $item[0];
                } else {
                    // 分割字符串
                    list($mainTopic, $subTopic) = explode('-', $item[0], 2);
                }
                if ($item[1] == '材料题') {
                    $newRow['title'] = $mainTopic . '.' . $item[2];
                } else {
                    if (!empty($subTopic)) {
                        $newRow['title'] = '（' . $subTopic . '）.' . $item[3];
                    } else {
                        $newRow['title'] = $mainTopic . '.' . $item[3];
                    }
                }

                if (empty($subTopic)) {
                    $datas[$mainTopic] = $newRow;
                } else {
                    $datas[$mainTopic]['sub_topic'][] = $newRow;
                }

            }
        }

        return $datas;
    }

    /**
     * 导入excel
     * Author MartinSun<syh@sunyonghong.com>
     * Date：   2017-10-12
     *
     * @param string $file_path excel文件的路径
     * @param boolean $toArray 是否返回数组数据
     * @return [type] [description]
     */
    public static function import($file_path = '', $toArray = true)
    {
        // 加载 Excel 文件
        $excel = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_path);

        // 如果不需要返回数组，直接返回 Excel 对象
        if ($toArray === false) {
            return $excel;
        }

        // 获取当前工作表
        $sheet = $excel->getActiveSheet();

        // 获取表格的最大行和最大列
        $highestRow = $sheet->getHighestRow();
        $highestColumn = $sheet->getHighestColumn();

        // 初始化数据数组
        $data = [];

        // 遍历所有单元格
        for ($row = 1; $row <= $highestRow; $row++) {
            $rowData = [];
            for ($col = 'A'; $col <= $highestColumn; $col++) {
                $cell = $sheet->getCell($col . $row);
                $richText = $cell->getValue();
                // 处理富文本的情况
                if ($richText instanceof \PhpOffice\PhpSpreadsheet\RichText\RichText) {
                    $formattedValue = '';
                    foreach ($richText->getRichTextElements() as $key=>$element) {
                        $font = $element->getFont();
                        $text = $element->getText();

                        if ($key==0){
                            $value = $cell->getValue();
                            $font = $cell->getStyle()->getFont();
                        }
                        // 检查是否有下划线
                        if ($font !== null && $font->getUnderline() !== \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE) {
                            // 如果有下划线，包裹 `<u>` 标签
                            $text = '<u>' . htmlspecialchars($text) . '</u>';
                        }

                        // 拼接富文本
                        $formattedValue .= $text;
                    }
                    $rowData[] = $formattedValue;
                } else {
                    // 检查值是否为 null
                    $cellValue = $cell->getValue();
                    // 如果单元格内容不为 null，则进行 htmlspecialchars 处理，否则设置为空字符串
                    $rowData[] = $cellValue !== null ? htmlspecialchars(strval($cellValue)) : '';
                }
            }
            $data[] = $rowData;
        }

        // 获取当前工作表中的所有图片
        $drawings = $sheet->getDrawingCollection();
        // 存储图片信息
        $images = [];
        // 创建 attachLogic 实例
        $attachLogic = new Attach();
        foreach ($drawings as $drawing) {
            $imageInfo = [];

            if ($drawing instanceof \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing) {
                ob_start();
                call_user_func(
                    $drawing->getRenderingFunction(),
                    $drawing->getImageResource()
                );
                $imageData = ob_get_contents();
                ob_end_clean();

                $extension = 'png';

                $savepath = $attachLogic->getUploadPath();
                $savename = md5(microtime()) . '.' . $extension;
                $outfile = $savepath . date('Ymd') . DIRECTORY_SEPARATOR . $savename;
                file_put_contents($outfile, $imageData);

                $file = new File($outfile);
                $filename = '试题图片' . $savename;
                $info = app(Attach::class)->saveAttach($file, ['filename' => $filename]);

                $cell = $drawing->getCoordinates();
                $imgSrc = $attachLogic->getAttachViewUrl($info->getAttr('id'));  // 使用实例调用

                $imageInfo = [
                    'img_tag' => '<img src="' . $imgSrc . '" />',
                    'cell'    => $cell
                ];

            } elseif ($drawing instanceof \PhpOffice\PhpSpreadsheet\Worksheet\Drawing) {
                // 如果图片是文件形式插入
                $zipReader = fopen($drawing->getPath(), 'r');
                $imageData = '';

                if ($zipReader !== false) {
                    while (!feof($zipReader)) {
                        $imageData .= fread($zipReader, 1024);
                    }
                    fclose($zipReader);
                    $extension = $drawing->getExtension();
                    // 生成保存路径
                    $savepath = $attachLogic->getUploadPath();
                    $savename = md5(microtime()) . '.' . $extension;
                    $outfile = $savepath . date('Ymd') . DIRECTORY_SEPARATOR . $savename;
                    // 保存图片到服务器
                    file_put_contents($outfile, $imageData);
                    // 生成图片访问 URL
                    $file = new File($outfile);
                    $filename = '试题图片' . $savename;
                    $info = app(Attach::class)->saveAttach($file, ['filename' => $filename]);
                    // 获取图片的起始单元格
                    $cell = $drawing->getCoordinates();

                    // 获取图片的链接
                    $imgSrc = $attachLogic->getAttachViewUrl($info->getAttr('id'));

                    // 保存图片信息和对应单元格
                    $imageInfo = [
                        'img_tag' => '<img src="' . $imgSrc . '" />',
                        'cell'    => $cell
                    ];
                }
            }

            // 将图片信息存储到数组中
            if (!empty($imageInfo)) {
                $images[] = $imageInfo;
            }
        }

        // 替换图片单元格中的内容
        foreach ($images as $image) {
            // 获取图片的单元格
            $cell = $image['cell'];
            // 将单元格转换为行号和列号
            preg_match('/([A-Z]+)([0-9]+)/', $cell, $matches);
            $columnLetter = $matches[1];  // 列字母，例如 "B"
            $rowNumber = $matches[2];     // 行号，例如 "3"
            // 将列字母转换为列索引 (A = 0, B = 1, C = 2, etc.)
            $columnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($columnLetter) - 1;
            // 将 `<img>` 标签替换到表格数据中
            $data[$rowNumber - 1][$columnIndex] .= $image['img_tag'];  // Excel 行列是从 1 开始，数组是从 0 开始
        }

        // 返回替换后的数据
        return $data;
    }





    /**
     * 解析节点
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/5/18
     *
     * @param $element
     * @param $handleImage
     * @return string
     * @throws Exception
     */
    protected function parseElement($element, $handleImage): string
    {
        $text = '';
        // 是否换行
        if (!$element instanceof TextBreak) {
            // if ($element instanceof ListItemRun) {
            // 取得单行数据
            foreach ($element->getElements() as $ele) {
                if ($ele instanceof Text) {
                    $text .= $ele->getText();
                } else if ($handleImage && $ele instanceof Image) {
                    // 匹配到图片且需要处理时
                    $savepath = $this->attachLogic->getUploadPath();
                    $extension = $ele->getImageExtension();
                    $savename = md5(microtime()) . '.' . $extension;
                    $outfile = $savepath . date('Ymd') . DIRECTORY_SEPARATOR . $savename;
                    // TODO 将图片根据配置储存,并使用图片的链接
                    $imageData = $ele->getImageStringData(true);
                    file_put_contents($outfile, base64_decode($imageData));
                    // 载入
                    $file = new File($outfile);
                    $filename = '试题图片' . $savename;
                    $info = app(Attach::class)->saveAttach($file, ['filename' => $filename]);
                    // $style     = $ele->getStyle();
                    $text .= '<img src="' . ($this->attachLogic->getAttachViewUrl($info->getAttr('id'))) . '" />';
                }
            }
            // } else if ($element instanceof TextRun) {
            //     foreach ($element->getElements() as $ele) {
            //         if ($ele instanceof Text) {
            //             $text .= $ele->getText();
            //         }
            //     }
            //
        }

        return $text;
    }

    /**
     * 解析单行数据
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/3/26
     *
     * @param string $text
     * @return array
     */
    private function parseLineText(string $text): array
    {
        // 英文(不区分大小写)+汉字
        if (preg_match_all('/【([A-Za-z\x80-\xff]{0,})】(.*)/', $text, $matchs)) {
            return [
                'field'           => $this->getFieldName($matchs[1][0]),
                'match_first_str' => $matchs[1][0],
                'value'           => $matchs[2][0]
            ];
        } else {
            return [
                'field' => '',
                'value' => $text
            ];
        }
    }

    /**
     * 获取字段名称
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/3/26
     *
     * @param string $title
     * @return string
     */
    private function getFieldName(string $title): string
    {
        $fields = [
            'title'      => ['单选题', '判断题', '多选题', '不定项选择题', '填空题', '材料题', '完形填空题', '简答题'],
            'answer'     => ['正确答案'],
            'point'      => ['知识点'],
            'difficulty' => ['难易度'],
            'analyze'    => ['题目解析']
        ];

        foreach ($fields as $field => $arr) {
            if (in_array($title, $arr)) {
                return $field;
            }
        }

        return 'options';
    }

    /**
     * 填充数据
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/3/29
     *
     * @Inject
     * @param array $data
     * @param array $result
     * @return array
     */
    private function appendItemData(array $data, array $result): array
    {
        if ($result) {
            $field = $result['field'];
            $value = $result['value'];
            // 如果指定了字段属性,则添加字段属性
            if ($field) {
                switch ($field) {
                    // 题干
                    case 'title';
                        $data['question_type'] = $result['match_first_str'];
                        $data[$field] = $value;
                        break;
                    // 选项
                    case 'options':
                        !isset($data['options']) && $data['options'] = [];
                        $data['options'][] = [
                            'key'   => $result['match_first_str'],
                            'value' => $value
                        ];
                        break;
                    // 其他属性: 答案/知识点/难易度/解析
                    default:
                        $data[$field] = $value;
                        break;
                }
            } else {
                // 如果未指定字段属性,则拼接上一个字段值
                $this->splicingItemData($data, $result['value']);
            }
        }

        return $data;
    }

    /**
     * splicingItemData
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/3/29
     *
     * @param array $data
     * @param string $value
     */
    private function splicingItemData(array &$data, string $value)
    {
        $lastKey = array_key_last($data);
        if ($lastKey !== null) {
            if (is_array($data[$lastKey])) {
                $this->splicingItemData($data[$lastKey], $value);
            } else {
                $lastKey && $data[$lastKey] .= $value;
            }

        }
    }

    /**
     * 解析试题数据格式
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/3/30
     *
     * @param array $datas
     * @param int $bankId
     * @return array
     */
    private function parseTopicDatas(array $datas, int $bankId = 0): array
    {
        $result = [];
        foreach ($datas as $data) {
            $questionType = $this->questionTopicLogic->getQuestionTypeByName($data['question_type']);
            $topic = [
                'bank_id'    => $bankId,
                'type'       => $questionType,
                'title'      => $data['title'],
                'analyze'    => $data['analyze'] ?? '',
                'point'      => $this->questionPointLogic->selectOrCreate($data['point'] ?? ''),
                'difficulty' => $this->questionDifficultLogic->findOrCreate($data['difficulty'] ?? '')
            ];
            switch ($questionType) {
                case 1: // 单选
                case 2:// 判断
                    $topic['options'] = $data['options'];
                    $topic['answer'] = $data['answer'];
                    break;
                case 3:// 多选
                case 4:// 不定项
                    $topic['options'] = $data['options'];
                    $topic['answers'] = array_unique(str_split($data['answer']));
                    break;
                // 材料
                case 6:
                    $topic['sub_topic'] = $this->parseTopicDatas($data['sub_topic'], $bankId);
                    break;
                // 简答
                case 8:
                    $topic['answer'] = $data['options'][0]['value'];
                    break;
            }

            $result[] = $topic;
        }

        return $result;
    }

    /**
     * 保存导入的试题
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/3/30
     *
     * @param array $datas
     * @return int
     */
    public function saveTopics(array $datas): int
    {
        $num = 0;
        foreach ($datas as $data) {
            $this->questionTopicLogic->createTopic($data);
            $num++;
        }

        return $num;
    }

    /**
     * 检测导入数据
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/5/6
     *
     * @param int $attachId
     * @return array|int[]
     */
    public function checkImport(int $attachId): array
    {
        $filepath = $this->attachLogic->where('id', $attachId)->findOrEmpty()->getAttr('file_path');
        if (!is_file($filepath)) {
            throw new LogicException('请重新上传试题文件');
        }
        $errors = [];
//        try {
            $datas = $this->readDatasFromExcelFile($filepath);

//            return ['status' => 0, 'errors' => $datas];
            foreach ($datas as $index => $data) {
                // 控制错误数量,错误达到一定数量,就不继续检测了
                if (count($errors) >= 10) {
                    break;
                }
                $number = $index + 1;
                if (!isset($data['question_type'])) {
                    $errors[] = '第' . $number . '道题：【暂不支持该试题类型】';
                    continue;
                }
                $questionType = $this->questionTopicLogic->getQuestionTypeByName($data['question_type']);
                switch ($questionType) {
                    case 1:
                    case 2:
                    case 3:
                    case 4:
                        $this->checkTopicItem($questionType, $data, $errors, $number);
                        break;
                    case 6:// 材料题
                        if (empty($data['sub_topic'])) {
                            $errors[] = '第' . $number . '道题：【至少有1个小题】';
                            break;
                        }
                        // 检查小题
                        foreach ($data['sub_topic'] as $subIndex => $subData) {
                            if (!isset($subData['question_type'])) {
                                $errors[] = '第' . $number . '大题的第（' . ($subIndex + 1) . '）小题：【暂不支持该试题类型】';
                                continue;
                            }
                            $questionType = $this->questionTopicLogic->getQuestionTypeByName($subData['question_type']);
                            $this->checkTopicItem($questionType, $subData, $errors, $number, $subIndex + 1);
                        }
                        break;
                }

            }

            if (empty($errors)) {
                return ['status' => 1];
            } else {
                return ['status' => 0, 'errors' => $errors];
            }
//        } catch (Exception $exception) {
//            throw new LogicException('请重新上传试题文件');
//        }

    }

    /**
     * 获取答案个数
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/5/6
     *
     * @param $answer
     * @return int
     */
    private function getAnswerCount($answer): int
    {
        return $answer ? count(str_split($answer)) : 0;
    }

    /**
     * 检测答案数据
     * Author: Martinsun <syh@sunyonghong.com>
     * Date: 2021/5/6
     *
     * @param     $questionType
     * @param     $data
     * @param     $errors
     * @param     $number
     * @param int $subNumber
     */
    private function checkTopicItem($questionType, $data, &$errors, $number, int $subNumber = 0)
    {
        $prefix = $subNumber ? '第' . $number . '大题的第（' . $subNumber . '）小题：' : '第' . $number . '道题：';
        switch ($questionType) {
            case 1:// 单选
                // 至少有2个答案选项
                if (count($data['options']) < 2) {
                    $errors[] = $prefix . '【至少有2个选项】';
                }
                // 只有一个正确答案
                $ansCount = $this->getAnswerCount($data['answer']);
                if ($ansCount != 1) {
                    $errors[] = $prefix . '【只能有1个正确答案或未设置正确答案】';
                }
                break;
            case 2:// 判断
                // 有2个答案选项
                if (count($data['options']) != 2) {
                    $errors[] = $prefix . '【只能有2个选项】';
                }
                // 只有一个正确答案
                $ansCount = $this->getAnswerCount($data['answer']);
                if ($ansCount != 1) {
                    $errors[] = $prefix . '【只能有1个正确答案或未设置正确答案】';
                }
                break;
            case 3:// 多选
                // 至少有2个答案选项
                if (count($data['options']) < 2) {
                    $errors[] = $prefix . '【至少有2个选项】';
                }
                // 至少有2个正确答案
                $ansCount = $this->getAnswerCount($data['answer']);
                if ($ansCount < 2) {
                    $errors[] = $prefix . '【至少有2个正确答案】';
                }
                break;
            case 4:// 不定项
                // 至少有2个答案选项
                if (count($data['options']) < 2) {
                    $errors[] = $prefix . '【至少有2个选项】';
                }
                // 至少有1个正确答案
                $ansCount = $this->getAnswerCount($data['answer']);
                if ($ansCount < 1) {
                    $errors[] = $prefix . '【至少有1个正确答案】';
                }
                break;
        }
        // 除简答题外
        if ($questionType != 8) {
            $opAnswer = array_column($data['options'], 'key');
            //  检测答案是否在选项中
            if ($data['answer']) {
                $answer = str_split($data['answer']);
                if (count(array_diff($answer, $opAnswer)) > 0) {
                    $errors[] = $prefix . '【正确答案未在给定的选项中】';
                }
            }
            // 检测选项是否重复
            $uq = array_unique($opAnswer);
            // 如果长度不相等,说明有重复值
            if (count($uq) !== count($opAnswer)) {
                $errors[] = $prefix . '【存在重复选项】';
            }

        }

    }
}
